package com.pinterest.ktlint.cli.reporter.baseline

import com.pinterest.ktlint.cli.reporter.baseline.Baseline.Status.INVALID
import com.pinterest.ktlint.cli.reporter.baseline.Baseline.Status.NOT_FOUND
import com.pinterest.ktlint.cli.reporter.baseline.Baseline.Status.VALID
import com.pinterest.ktlint.cli.reporter.core.api.KtlintCliError
import com.pinterest.ktlint.cli.reporter.core.api.KtlintCliError.Status.BASELINE_IGNORED
import com.pinterest.ktlint.logger.api.initKtLintKLogger
import com.pinterest.ktlint.rule.engine.core.api.RuleId
import io.github.oshai.kotlinlogging.KotlinLogging
import org.w3c.dom.Element
import org.xml.sax.SAXException
import java.io.IOException
import java.io.InputStream
import java.nio.file.Paths
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.ParserConfigurationException

private val LOGGER = KotlinLogging.logger {}.initKtLintKLogger()

/**
 * Baseline of lint errors to be ignored in subsequent calls to ktlint.
 */
public class Baseline(
    /**
     * Path to the baseline file.
     */
    public val path: String? = null,
    /**
     * Status of the baseline file.
     */
    public val status: Status,
    /**
     * Lint errors grouped by (relative) file path.
     */
    public val lintErrorsPerFile: Map<String, List<KtlintCliError>> = emptyMap(),
) {
    public enum class Status {
        /**
         * Consumer did not request the Baseline file to be loaded.
         */
        DISABLED,

        /**
         * Baseline file is successfully parsed.
         */
        VALID,

        /**
         * Baseline file does not exist. File needs to be generated by the consumer first.
         */
        NOT_FOUND,

        /**
         * Baseline file is not successfully parsed. File needs to be regenerated by the consumer.
         */
        INVALID,
    }
}

public enum class BaselineErrorHandling {
    /**
     * Log an error message. Does not throw an exception.
     */
    LOG,

    /**
     * Throws an exception on error. Does not log the error.
     */
    EXCEPTION,
}

/**
 * Loads the [Baseline] from the file located on [path]. Exceptions are swallowed and log message is written. On error, the baseline file is
 * deleted.
 */
@Deprecated(
    message = "Marked for removal in Ktlint 2.0",
    replaceWith = ReplaceWith("loadBaseline(path, BaselineErrorHandling.LOG)"),
)
public fun loadBaseline(path: String): Baseline = loadBaseline(path, BaselineErrorHandling.LOG)

/**
 * Loads the [Baseline] from the file located on [path]. In case the baseline file can not be loaded successfully, it will be deleted.
 */
public fun loadBaseline(
    path: String,
    errorHandling: BaselineErrorHandling = BaselineErrorHandling.EXCEPTION,
): Baseline =
    with(BaselineLoader(path)) {
        try {
            load()
        } catch (e: Exception) {
            // Delete baseline as it contains an error
            try {
                delete()
            } catch (e: Exception) {
                if (errorHandling == BaselineErrorHandling.LOG) {
                    LOGGER.error { e.message }
                } else {
                    // Swallow as original exception from loading is to be returned only
                }
            }

            // Handle original exception
            if (errorHandling == BaselineErrorHandling.EXCEPTION) {
                throw e
            } else {
                LOGGER.error { e.message }
                Baseline(path = path, status = INVALID)
            }
        }
    }

private class BaselineLoader(
    private val path: String,
) {
    private val baselinePath =
        Paths
            .get(path)
            .toFile()
            .takeIf { it.exists() }

    var ruleReferenceWithoutRuleSetIdPrefix = 0

    fun load(): Baseline {
        require(path.isNotBlank()) { "Path for loading baseline may not be blank or empty" }

        baselinePath
            ?.let { baselineFile ->
                try {
                    return Baseline(
                        path = path,
                        lintErrorsPerFile = baselineFile.inputStream().parseBaseline(),
                        status = VALID,
                    ).also {
                        if (ruleReferenceWithoutRuleSetIdPrefix > 0) {
                            LOGGER.warn {
                                "Baseline file '$path' contains $ruleReferenceWithoutRuleSetIdPrefix reference(s) to rule ids without " +
                                    "a rule set id. For those references the rule set id 'standard' is assumed. It is advised to " +
                                    "regenerate this baseline file."
                            }
                        }
                    }
                } catch (e: IOException) {
                    throw BaselineLoaderException("Unable to parse baseline file: $path", e)
                } catch (e: ParserConfigurationException) {
                    throw BaselineLoaderException("Unable to parse baseline file: $path", e)
                } catch (e: SAXException) {
                    throw BaselineLoaderException("Unable to parse baseline file: $path", e)
                }
            }

        return Baseline(path = path, status = NOT_FOUND)
    }

    /**
     * Parses the [InputStream] of a baseline file and return the lint errors grouped by the relative file names.
     */
    private fun InputStream.parseBaseline(): Map<String, List<KtlintCliError>> {
        val lintErrorsPerFile = HashMap<String, List<KtlintCliError>>()
        with(parseDocument().getElementsByTagName("file")) {
            for (i in 0 until length) {
                with(item(i) as Element) {
                    val fileName = getAttribute("name")
                    lintErrorsPerFile[fileName] = parseBaselineFileElement()
                }
            }
        }
        return lintErrorsPerFile
    }

    private fun InputStream.parseDocument() =
        DocumentBuilderFactory
            .newInstance()
            .newDocumentBuilder()
            .parse(this)

    /**
     * Parses a "file" [Element] in the baseline file.
     */
    private fun Element.parseBaselineFileElement(): List<KtlintCliError> {
        val ktlintCliErrorsInFileElement = mutableListOf<KtlintCliError>()
        with(getElementsByTagName("error")) {
            for (i in 0 until length) {
                ktlintCliErrorsInFileElement.add(
                    with(item(i) as Element) {
                        parseBaselineErrorElement()
                    },
                )
            }
        }
        return ktlintCliErrorsInFileElement
    }

    /**
     * Parses an "error" [Element] in the baseline file.
     */
    private fun Element.parseBaselineErrorElement() =
        KtlintCliError(
            line = getAttribute("line").toInt(),
            col = getAttribute("column").toInt(),
            ruleId =
                getAttribute("source")
                    .let { ruleId ->
                        // Ensure backwards compatibility with baseline files in which the rule set id for standard rules is not saved
                        RuleId
                            .prefixWithStandardRuleSetIdWhenMissing(ruleId)
                            .also { prefixedRuleId ->
                                if (prefixedRuleId != ruleId) {
                                    ruleReferenceWithoutRuleSetIdPrefix++
                                }
                            }
                    },
            // Detail is not available in the baseline
            detail = "",
            status = BASELINE_IGNORED,
        )

    fun delete() {
        try {
            baselinePath?.delete()
        } catch (e: IOException) {
            throw BaselineLoaderException("Unable to delete baseline file: $path", e)
        }
    }
}

public class BaselineLoaderException(
    message: String,
    throwable: Throwable,
) : RuntimeException(message, throwable)

/**
 * Checks if the list contains the given [KtlintCliError]. The [List.contains] function can not be used as [KtlintCliError.detail] is not
 * available in the baseline file and a normal equality check on the [KtlintCliError] fails.
 */
public fun List<KtlintCliError>.containsLintError(ktlintCliError: KtlintCliError): Boolean = any { it.isSameAs(ktlintCliError) }

private fun KtlintCliError.isSameAs(lintError: KtlintCliError) =
    col == lintError.col &&
        line == lintError.line &&
        RuleId.prefixWithStandardRuleSetIdWhenMissing(ruleId) == RuleId.prefixWithStandardRuleSetIdWhenMissing(lintError.ruleId)

/**
 * Checks if the list does not contain the given [KtlintCliError]. The [List.contains] function can not be used as [KtlintCliError.detail]
 * is not available in the baseline file and a normal equality check on the [KtlintCliError] fails.
 */
public fun List<KtlintCliError>.doesNotContain(ktlintCliError: KtlintCliError): Boolean = none { it.isSameAs(ktlintCliError) }
